Skip to content

fix(codegen-spring): make ejected generators compile when ejected alone - #430

Merged
dmealing merged 4 commits into
mainfrom
fm/mo-1-1-0-java-eject-alone
Oct 11, 2026
Merged

dmealing merged 4 commits into
mainfrom
fm/mo-1-1-0-java-eject-alone

Conversation

@dmealing

@dmealing dmealing commented Oct 11, 2026 •

Copy link
Copy Markdown
Member

What Changed

  • Java: moves the shared Spring emit rules into a public SpringGeneratorSupport that every ejectable Spring generator imports. metaobjects:eject copies it as an owned file beside the ejected generators, so routes, dto and value-object compile when ejected alone. Generated output is unchanged. -Dlist and metaobjects:verify treat the copy like other owned runtime files.
  • Java: AutoSetSupport selectors from 1.0.x (stampFields, onUpdateFields, hasAutoSetFields, hasOnUpdateFields) are restored as @Deprecated and self-contained. The 1.1.0 upgrade notes add a Java re-eject step.
  • Python: filter-allowlist and extractor no longer import helpers from sibling generators. The shared predicates ship as shared_predicates through the runtime= eject mechanism.
  • Kotlin: small source edits to the entity and M2M generators, plus a behavioural test that ejects each registered generator alone and compiles the result. Java and Python get matching eject-alone tests.
  • Kotlin result: compared the public and protected members of KotlinEntityGenerator.kt and KotlinM2mSupport.kt, the two Kotlin files this PR touches, between v1.0.13 and this head. No published helper was removed or changed signature, so no Kotlin forwarder is needed. The edits are two comment references.
  • Docs: CHANGELOG 1.1.0 Fixed entry, the own-your-codegen feature doc, and the Java and Kotlin port notes.

Validation runs on this branch: 2.

Validation

Risk Assessment

✅ Low: The helper move is a body-for-body relocation with every 1.0.13 public and protected signature restored as a forwarder, the eject, list and verify paths trace correctly for first eject, later eject and forced re-eject, the Python gate change in render_router is equivalent to the old chain, and the only finding is a test-coverage gap with no current defect; no tests were run in this review.

Testing

Baseline ts-fast/ts-unit already green. Drove the eject path through its real entry points: Java MetaDataEjectMojo in-process via EjectAloneRoundTripTest (6 tests: every registry generator ejected alone and javac-compiled with packaged support hidden; byte-identical output; owned-edit propagation). Kotlin EjectedGeneratorAloneCompilesTest passed. Spring AutoSetSupportLegacyFormsTest, SpringGeneratorLegacyFormsTest, SpringDtoGeneratorExtensibilityTest and OwnedPathNeverReachesPackagedSupportTest passed. Python test_eject_independence.py and test_eject_runtime.py passed (13 passed; 2 runtime route tests skipped, fastapi missing). Regression: full Python suite minus tests/integration passed (2829 passed, 2 skipped); full mvn -o test for maven-plugin and codegen-spring passed. Untested: tests/integration (needs fastapi and pg8000, not in offline cache). Cleaned uv.lock edits, .venv and target dirs. No dollar spend; subscription quota only.

  • Live validation: ✅ go - 7 of 9 scenarios driven live against the product
Scenario Result Live Evidence
Java routes, dto and value-object each ejected alone via mvn metaobjects:eject compile without the packaged support class ✅ pass live EjectAloneRoundTripTest.everyJavaGeneratorEjectedAloneCompilesWithoutThePackagedSupport (surefire report maven-plugin/target, removed after run)
Ejected Java copies emit byte-identical output to packaged generators ✅ pass live EjectAloneRoundTripTest unchanged-output test (6/6 passing)
Editing owned support copy changes routes, dto and stamp output together ✅ pass live EjectAloneRoundTripTest owned-edit test
Kotlin generators ejected alone compile ✅ pass live EjectedGeneratorAloneCompilesTest (codegen-kotlin)
Python filter-allowlist and extractor eject and import in a fresh interpreter with packaged module blocked ✅ pass live server/python tests/codegen/test_eject_independence.py
Python route and runtime tests for ejected copies ⏸️ untested no fastapi not in offline uv cache; network disabled so could not install. Provide fastapi in the offline cache or allow network to run.
Java 1.0.x owned copies (routes/dto/repository calls) still compile against 1.1 classes ✅ pass live AutoSetSupportLegacyFormsTest and SpringGeneratorLegacyFormsTest (codegen-spring)
No packaged class reachable from owned generators references SpringGeneratorSupport ✅ pass live OwnedPathNeverReachesPackagedSupportTest (bytecode walk)
Full python integration suite (tests/integration) ⏸️ untested no fastapi and pg8000 missing from offline cache; not installable with network disabled.

Pipeline

Updates from git push no-mistakes

✅ **Intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ⚠️ CHANGELOG.md:63 - The 1.1.0 upgrade bullet "An ejected Java Spring generator needs a re-eject" (CHANGELOG.md:63-68) states that a copy of routes, dto, repository or value-object made before 1.1.0 "calls static helpers on the packaged SpringDtoGenerator and SpringRepositoryGenerator ... so it stops compiling after the upgrade". The source does not support that. A v1.0.13 copy has no import of either sibling (v1.0.13 SpringControllerGenerator.java:7-11, SpringDtoGenerator.java:7-12, SpringValueObjectGenerator.java:7-9), so SpringDtoGenerator.x(...) in an owned copy resolves only to the owned sibling in the same package, never to the packaged class. Concrete case: the trio routes,dto,repository ejected together on 8.0.13 (the one combination that compiled) calls its own SpringDtoGenerator / SpringRepositoryGenerator, and its only calls into a packaged class this change touched are the four AutoSetSupport selectors (v1.0.13 controller :324,:373,:1026,:1076; dto :220,:244,:268,:521), which this change restores as @Deprecated. That trio still compiles on 8.1.0. repository alone calls neither class. The bullet also contradicts the same file at CHANGELOG.md:883-886 ("still there, marked @Deprecated, so code that calls them still compiles"), the comment at AutoSetSupport.java:91-97, the javadoc of AutoSetSupportLegacyFormsTest, and the recorded round 3 decision ("Add the Java step to the 1.1.0 upgrade notes: old owned copies keep compiling; re-eject with -Dforce=true to own the rules"). The only copy that does stop compiling is one the adopter repaired by hand with an import of the packaged SpringDtoGenerator or SpringRepositoryGenerator (for example dto alone plus an import for appliesTo). Fix: reword the bullet to say that copies ejected before 1.1.0 keep compiling and keep applying the 1.0.x rules; re-eject with -Dforce=true to own the shared rules in SpringGeneratorSupport; a copy edited to import the packaged SpringDtoGenerator or SpringRepositoryGenerator must be re-ejected, because those helpers moved. Keep the git diff merge advice. No other file repeats the wrong claim.

🔧 Fix applied.
2 warnings still open:

  • ⚠️ CHANGELOG.md:63 - The 1.1.0 upgrade bullet "An ejected Java Spring generator needs a re-eject" (CHANGELOG.md:63-68) states that a copy of routes, dto, repository or value-object made before 1.1.0 "calls static helpers on the packaged SpringDtoGenerator and SpringRepositoryGenerator ... so it stops compiling after the upgrade". The source does not support that. A v1.0.13 copy has no import of either sibling (v1.0.13 SpringControllerGenerator.java:7-11, SpringDtoGenerator.java:7-12, SpringValueObjectGenerator.java:7-9), so SpringDtoGenerator.x(...) in an owned copy resolves only to the owned sibling in the same package, never to the packaged class. Concrete case: the trio routes,dto,repository ejected together on 8.0.13 (the one combination that compiled) calls its own SpringDtoGenerator / SpringRepositoryGenerator, and its only calls into a packaged class this change touched are the four AutoSetSupport selectors (v1.0.13 controller :324,:373,:1026,:1076; dto :220,:244,:268,:521), which this change restores as @Deprecated. That trio still compiles on 8.1.0. repository alone calls neither class. The bullet also contradicts the same file at CHANGELOG.md:883-886 ("still there, marked @Deprecated, so code that calls them still compiles"), the comment at AutoSetSupport.java:91-97, the javadoc of AutoSetSupportLegacyFormsTest, and the recorded round 3 decision ("Add the Java step to the 1.1.0 upgrade notes: old owned copies keep compiling; re-eject with -Dforce=true to own the rules"). The only copy that does stop compiling is one the adopter repaired by hand with an import of the packaged SpringDtoGenerator or SpringRepositoryGenerator (for example dto alone plus an import for appliesTo). Fix: reword the bullet to say that copies ejected before 1.1.0 keep compiling and keep applying the 1.0.x rules; re-eject with -Dforce=true to own the shared rules in SpringGeneratorSupport; a copy edited to import the packaged SpringDtoGenerator or SpringRepositoryGenerator must be re-ejected, because those helpers moved. Keep the git diff merge advice. No other file repeats the wrong claim.
  • ⚠️ CHANGELOG.md:888 - The change removes published static helpers from two packaged generator classes in a minor release, and only the four AutoSetSupport selectors got a compatibility shim. Removed since v1.0.13: on SpringDtoGenerator, public settableFields(MetaObject, String), scalarFields, dtoComponentFields, valueObjectJsonbFields, valueObjectMapFields, isValueObjectJsonbField, componentType, isRequired, originGuaranteedNonNull, validationAnnotations, and protected sizeAnnotation, attrBool, attrInt, hasValidator, escapeJavaString; on SpringRepositoryGenerator, public appliesTo, m2mFinderName, m2mFinderNameForSubtype, targetSubtypeParam. Concrete sequence: an adopter on 8.0.13 subclasses SpringDtoGenerator (the extension contract SpringDtoGeneratorExtensibilityTest pins) and calls the inherited validationAnnotations(field) or componentType(field, owner), or their own generator calls SpringDtoGenerator.dtoComponentFields(entity) as JavaFieldShapes did. After upgrading to 8.1.0 that code fails with cannot find symbol. CHANGELOG.md:888-890 tells such callers to switch to SpringGeneratorSupport, but the break is not in the "Four diffs to expect when you upgrade" list, and the round 3 decision on the sibling case said "no source break in a minor release". This is not from the latest fix round; the original change introduced it and round 3 closed only the AutoSetSupport sibling. Unlike that case, @Deprecated forwarders on these two classes create no back-edge: both are ejectable generators that already import SpringGeneratorSupport, and a forwarder to the support class is not a generator-to-generator reference. The remedy needs a decision, not the defect: either add @Deprecated forwarders with the 1.0.13 signatures on both classes, or accept the break and list it under the upgrade diffs. If one of the older decisions omitted from my history already accepted this break, disregard.

🔧 Fix applied.
1 info still open:

  • ℹ️ server/java/codegen-spring/src/test/java/com/metaobjects/generator/spring/OwnedPathNeverReachesPackagedSupportTest.java:92 - Test gap opened by fix round 2 (the restored forwarders); no defect in the current code. noPackagedClassAnOwnedGeneratorReachesRefersBackToTheSupportClass flags a reached packaged class only when it refers DIRECTLY to SpringGeneratorSupport (line 89, 92-94). It puts the packaged ejectable generators in the owned set (line 76-77), so a reference to one of them is never a back-edge. Before round 2 that was safe: the helpers were gone from SpringDtoGenerator and SpringRepositoryGenerator, so no packaged class could call them. Round 2 restored 19 @Deprecated static forwarders on those two classes (SpringDtoGenerator.java:910-998, SpringRepositoryGenerator.java:477-499), each calling the packaged SpringGeneratorSupport. Plausible wrong behaviour the test now accepts: a packaged engine class an owned generator calls (TphPlan, SpringTypeMapper, AutoSetSupport, SpringM2mSupport, Fr019SharedEnum) calls SpringDtoGenerator.dtoComponentFields(entity). That compiles again, runs the packaged rule behind an owned generator, and the walk reports no back-edge. The sibling test at line 108 covers only generator-to-generator references, not engine-to-generator. I checked the source: today no packaged class outside apidocs and GeneratorRegistry refers to any ejectable generator, and neither of those is reached from an owned generator, so the invariant holds now. Fix: in the walk, also record a back-edge when a reached non-owned class refers to an ejectable generator class (ejectableGenerators().contains(outer(ref))). The check passes on the current tree.
✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 7 of 9 scenarios driven live against the product
Scenario Result Live Evidence
Java routes, dto and value-object each ejected alone via mvn metaobjects:eject compile without the packaged support class ✅ pass live EjectAloneRoundTripTest.everyJavaGeneratorEjectedAloneCompilesWithoutThePackagedSupport (surefire report maven-plugin/target, removed after run)
Ejected Java copies emit byte-identical output to packaged generators ✅ pass live EjectAloneRoundTripTest unchanged-output test (6/6 passing)
Editing owned support copy changes routes, dto and stamp output together ✅ pass live EjectAloneRoundTripTest owned-edit test
Kotlin generators ejected alone compile ✅ pass live EjectedGeneratorAloneCompilesTest (codegen-kotlin)
Python filter-allowlist and extractor eject and import in a fresh interpreter with packaged module blocked ✅ pass live server/python tests/codegen/test_eject_independence.py
Python route and runtime tests for ejected copies ⏸️ untested no fastapi not in offline uv cache; network disabled so could not install. Provide fastapi in the offline cache or allow network to run.
Java 1.0.x owned copies (routes/dto/repository calls) still compile against 1.1 classes ✅ pass live AutoSetSupportLegacyFormsTest and SpringGeneratorLegacyFormsTest (codegen-spring)
No packaged class reachable from owned generators references SpringGeneratorSupport ✅ pass live OwnedPathNeverReachesPackagedSupportTest (bytecode walk)
Full python integration suite (tests/integration) ⏸️ untested no fastapi and pg8000 missing from offline cache; not installable with network disabled.
  • scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains
  • scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains (baseline, already green)
  • cd server/java && mvn -o -q -pl maven-plugin -am test -Dtest=EjectAloneRoundTripTest (6 tests, pass)
  • cd server/java && mvn -o -q -pl codegen-spring test -Dtest=OwnedPathNeverReachesPackagedSupportTest,AutoSetSupportLegacyFormsTest (pass)
  • cd server/java && mvn -o -q -pl codegen-kotlin test -Dtest=EjectedGeneratorAloneCompilesTest (pass)
  • cd server/python && uv run --offline pytest -q tests/codegen/test_eject_independence.py tests/codegen/test_eject_runtime.py (13 passed, 2 skipped: fastapi missing)
  • cd server/python && uv run --offline pytest -q --ignore=tests/integration (2829 passed, 2 skipped)
  • cd server/java && mvn -o -q -pl maven-plugin,codegen-spring -am test (full regression, pass)
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

…ed alone

An ejected Java routes, dto or value-object copy called static helpers on the packaged
SpringDtoGenerator and SpringRepositoryGenerator with no import, and eject rewrote only the
package line, so each failed to compile on its own (12, 3 and 3 errors). The shared emit rules
now sit in one public SpringGeneratorSupport that every ejectable Spring generator imports,
and eject copies it beside the owned generators, once, honouring -Dforce, with -Dlist and
metaobjects:verify treating it like an owned runtime file. No packaged class an owned
generator reaches refers to it. The four 1.0.x AutoSetSupport selectors stay as deprecated,
self-contained methods. In Python, emits_router and has_literal_default moved to a shared
predicates module that metaobjects eject copies through the runtime mechanism. Tests eject
each registered generator alone in Java and Kotlin, and import each Python generator in a
fresh interpreter. Generated output is unchanged.
@dmealing
dmealing merged commit c3aacf9 into main Oct 11, 2026
1 check passed
@dmealing
dmealing deleted the fm/mo-1-1-0-java-eject-alone branch October 11, 2026 20:13
dmealing added a commit that referenced this pull request Oct 11, 2026
Brings in #429 and #430. One conflict, in CHANGELOG.md: both sides appended
entries to the end of the [1.1.0] Fixed list, and both are kept.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant